/*
    This file is part of the Foothill College Web GIS.

    Foothill College Web GIS is free software: you can redistribute it 
    and/or modify it under the terms of the GNU General Public License 
    as published by the Free Software Foundation, either version 3 of 
    the License, or (at your option) any later version.

    Foothill College Web GIS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Foothill College Web GIS. If not, see 
    <http://www.gnu.org/licenses/>.

*/

// It's an object; this is the only thing that should go in the global namespace
// All variables should be either var variablename = ... or this.varname = ...
// NOTHING should be variablename = ... (this is a global variable)
// Also, jQuery must be referenced as jQuery, not $, in case it's ever used
// on a site where they use another Javascript framework that also uses $
// Call it like new fhWebMap( id of element where the map should be drawn )
// fhWebMap.init()
function fhWebMap (mapElemId, options) {
    // INIT STUFF
    // We'll need this later
    this.mapElemId = mapElemId;

    // Allow people to not pass in an options array and accept the defaults
    if (options == undefined) options = {};

    this.options = options;

    // Parse the options hash
    // namespace: the DOM ID namespace. Only needs to be changed when there are
    // multiple maps on a page.
    this.namespace = options.namespace?options.namespace:'fhWebMap';

    // has to be != undefined; otherwise, setting geocode to false would
    // cause it to be set to true!
    this.options.geocode = (this.options.geocode != undefined)?this.options.geocode:true;
    this.options.geocodeBox = (this._undefined(this.options.geocodeBox))?true:this.options.geocodeBox;
    // Where geocode.kml is - must be on same domain
    this.options.geocodeDB = this.options.geocodeDB?this.options.geocodeDB:'geocode.kml';
    this.options.layers = options.layers?options.layers:['buildings', 'rooms', 'restrooms', 'elevators'];
    this.options.zoomLevel = options.zoomLevel?options.zoomLevel:'detect';
    this.options.legend = (this._undefined(this.options.legend))?true:this.options.legend;
    
    // this.options.noCache is also available.

    // These variables keeps track of state
    this.layerInFront = null;
    this.iconIndex = 0; // What the next icon drawn should be
}

fhWebMap.prototype.log = function (msg) {
    // use the JS console if available
    // use window.console because if you try to pass in plain console, in many browsers it will crash saying the variable is undefined
    if (this._undefined(window.console) == false) {
	console.log(msg);
    }
    else {
	// log to the log area div, which is defined if console is undefined
	jQuery('#' + this.namespace + '_logArea').append(msg + '<br/>');
    }
}// this.log

// Draw a pretty alert box over the map
fhWebMap.prototype.alert = function (text) {
    var msgBox = jQuery('<div id="' + this.namespace + '_alert"' +
			'class="fhWebMap_alert">' + text + '<br/>' +
			'<input type="button" value="OK" />' +
			'</div>');

    var map = jQuery('#' + this.mapElemId);
    map.prepend(msgBox);

    // center it up
    msgBox.css('left', ((map.width ()/2) - (msgBox.width ()/2)) + 'px');
    msgBox.css('top',  ((map.height()/2) - (msgBox.height()/2)) + 'px');

    msgBox.find('input').click(function (e) {
	e.preventDefault();
	msgBox.remove();
    });
}

// draw the initial map
fhWebMap.prototype.init = function () {
    // Let's see if I can explain why this is necessary - it's kind of 
    // black magic. We use a closure (in the form of an anonymous function)
    // in the many statements below, and within the scope of those 
    // functions, `this` refers to the them. But local variables are 
    // included in a closure's namespace, so setting 
    // instance allows us to access class
    // methods of fhWebMap as instance.<whatever>
    var instance = this;

    // make the request first, so there's some chance it may have loaded
    // when we need it
    if (this.options.geocode) {
	jQuery.ajax({
	    url: this.options.geocodeDB,
	    format: 'xml',
	    success: function (data) {
		// save it
		instance.geocodeDB = jQuery(data);
	    },
	    error: function (data) {
		instance.alert("\
Could not load the geocoding database. If you're the server admin, \
make sure that geocode.kml is in the same directory as the page, or else has a \
URL specified. Note it must be on the same domain."
			      );
	    }
	});
    } // if (this.options.geocode)

    // Add the HTML features to the containing div
    // All element ids should start with fhWebMap, to keep things clean
    var html = '';
    html += '<div id="' + this.namespace + '_map" style="width: 100%; height: 100%"></div>';
    html += '<div id="' + this.namespace + '_legend" class="fhWebMap_legend" style="display: none"></div>';

    // Build a div to put log messages in if console.log is undefined. This can be hidden with CSS when not doing development
    if (this._undefined(window.console)) {
	html += '<div id="' + this.namespace + '_logArea" class="fhWebMap_logArea">';
    }
    
    // We put a clearing div at the bottom because elements are often
    // floated over the map (e.g. layer selector), and we don't want them
    // to mess up other parts of the page.
    html += '<div style="clear:both"></div>';


    // Put it in, overwriting anything else
    var ourDiv = jQuery('#' + this.mapElemId);
    ourDiv.html(html);
    
    // And add our class to the div
    // Don't overwrite other classes
    ourDiv.attr('class', ourDiv.attr('class') + ' fhWebMap');

    // Calculate the initial zoom level based on screen size so that
    // the entire campus is shown, unless it's been overridden
    if (this.options.zoomLevel == 'detect') {
	// the map will fill the div, even though there are some other
	// elements drawn over it
	var w = ourDiv.width();
	var h = ourDiv.height();

	if     (w > 1800 && h > 650) var zoomLevel = 18;
	else if (w > 750 && h > 525) var zoomLevel = 17;
	else if (w > 379 && h > 260) var zoomLevel = 16;
	else                         var zoomLevel = 15;
    }
    else {
	// It's been overridden
	var zoomLevel = this.options.zoomLevel;
    }

    var foothillLatLng = new google.maps.LatLng(37.36126, -122.12871);
    var mapOpts = {
	zoom: zoomLevel,
	center: foothillLatLng,
	mapTypeId: google.maps.MapTypeId.SATELLITE,
	// 45 degree imagery has numerous problems - 
	// see issues 3 and 10. Our map doesn't overlay correctly,
	// and 1300 and 4400 are shown even though they've been demolished.
	tilt: 0 
    };
    
    this.map = new google.maps.Map(
	// Didn't use jQuery('#' + mapElemId) because it returns a
	// jQuery object, not a DOM object, and I'm not sure if that
	// will play nice with Google Maps
	document.getElementById(this.namespace + '_map'),
	mapOpts
    );

    this.layers = {};
    this.placemarks = {};

    var kmlLayerOptions = {
	preserveViewport: true, // Shouldn't change viewport when we toggle
	suppressInfoWindows: true // we draw our own info windows, so that
	// they can be exclusive
    };

    // We append this random number to each SVN url to force the
    // KML files to be fetched new each time
    if (this.options.noCache) var randstr = '?random=' + Math.random();
    else var randstr = '';

    // Add the Parking Lots layer
    // this one is the easiest; no multiple levels!
    this.layers.parking = new google.maps.KmlLayer(this.urls.data + '/parking.kmz' + randstr, kmlLayerOptions);
    this.log('added parking lots');

    // Add the building envelopes layer
    // again, no levels
    this.layers.buildings = new google.maps.KmlLayer(this.urls.data + '/buildings.kmz' + randstr, kmlLayerOptions);
    this.log('added building footprints');

    // Rooms are three levels
    this.layers.rooms1 = new google.maps.KmlLayer(
	this.urls.data + '/rooms1.kmz' + randstr, 
	kmlLayerOptions
    );
    this.layers.rooms2 = new google.maps.KmlLayer(
	this.urls.data + '/rooms2.kmz' + randstr, 
	kmlLayerOptions
    );
    this.layers.rooms3 = new google.maps.KmlLayer(
	this.urls.data + '/rooms3.kmz' + randstr, 
	kmlLayerOptions
    );

    // Note: these two are kmz b/c they have icons
    this.layers.elevators = new google.maps.KmlLayer(
	this.urls.data + '/elevators.kmz' + randstr, 
	kmlLayerOptions
    );
    this.layers.restrooms = new google.maps.KmlLayer(
	this.urls.data + '/restrooms.kmz' + randstr, 
	kmlLayerOptions
    );
    this.log('added rooms');

    // Add a click listener to display the info boxes
    // we don't use the default boxes because they can't be controlled
    // by this.infoWindow because we didn't create them
    // the reason we us a closure is that otherwise the scope of showInfoWindow
    // will be the google Map and not fhWebMap
    google.maps.event.addListener(this.layers.rooms1, 'click', function (pm) {
	instance.showInfoWindow(pm);
    });
    google.maps.event.addListener(this.layers.rooms2, 'click', function (pm) {
	instance.showInfoWindow(pm);
    });
    google.maps.event.addListener(this.layers.rooms3, 'click', function (pm) {
	instance.showInfoWindow(pm);
    });

    // Add all the layers they requested
    // note it is this.options.layers, not just this.layers!
    // They mean different things!
    // We don't use this.options.layers.forEach anymore because IE doesn't support it.
    // We set this variable and then refer to it so that we don't have to check the length of the list on each iteration (doesn't matter much here, but in a long
    // list it could)
    var listLen = this.options.layers.length;
    for (var i = 0; i < listLen; i++) {
	var layerName = this.options.layers[i];
	// If restrooms or elevators are requested they will be shown when
	// we call showLayer('rooms'). The reason we do this is that these
	// layers *must not* be loaded until *after* rooms3 loads. Otherwise,
	// they are drawn underneath (not useful)!
	if (layerName != 'elevators' & layerName != 'restrooms') {
	    this.showLayer(layerName);
	}
    }

    var ns = this.namespace; // to condense the code below

    /* This is a little convenience function we use in the HTML code
       below. It returns 'checked="true"' if the layer name is shown,
       '' if it isn't. The reason we have to do this is that some browsers
       (ok, most browsers) draw the checkbox as checked if the value of
       checked is anything,*including* "false". */
    function checked(layerName) {
	// Use instance instead of this because this refers to this func
   // Don't use _shown, because layers may not have started loading (this is true for rooms, since each one waits
   // for the previous one to draw. When this gets called, setMap has only been called on rooms1, but all of the rooms will be
   // displayed eventually)
	if (jQuery.inArray(layerName, instance.options.layers) > -1) return 'checked="true"';
	else return '';
    }

    // This is the actual selection dialog box
    // the reason we put in value=... is so that we know what
    // layer was checked/unchecked in the event handler

    var control = jQuery('\
<div id="' + ns + '_layerSelector" class="fhWebMap_widget fhWebMap_layerSelector">\
<!-- Wrap all this in a span so that we can get the width of a non block\
element. No need for ID; we can refer to as #ns_LayerSelector span -->\
<span>\
<a href="#" id="' + ns + '_layerSelectorButton">Layers</a><br/>\
<div id="' + ns + '_layerSelectorSlideBox" style="display: none" \
class="fhWebMap_layerSelector fhWebMap_slideBox">\
<input type="checkbox" ' + checked('parking') + '" \
id="' + ns + '_layerSelectorParking" \
class="' + ns + '_layerSelectorBox" value="parking" />\
Parking Lots<br/>\
<input type="checkbox" ' + checked('buildings') + '" \
id="' + ns + '_layerSelectorBuildings" \
class="' + ns + '_layerSelectorBox" value="buildings" />\
Buildings<br/>\
<input type="checkbox" ' + checked('rooms') + '" \
id="' + ns + '_layerSelectorRooms" \
class="' + ns + '_layerSelectorBox" value="rooms" />Rooms<br/>\
<input type="checkbox" ' + checked('restrooms') + '" \
id="' + ns + '_layerSelectorRestrooms" \
class="' + ns + '_layerSelectorBox" value="restrooms" />\
Restrooms<br/>\
<input type="checkbox" ' + checked('elevators') + '" \
id="' + ns + '_layerSelectorElevators" \
class="' + ns + '_layerSelectorBox" value="elevators" />\
Elevators<br/>\
</div></span></div>');

    // bind the events

    // first, the hide/show layer handler event
    // we do this with the control variable, not after we add it to the map,
    // because it is added asynchronously and who knows how long it might
    // take for that to happen, so we can't rely on the elements being
    // created when we assign handlers, if we do it that way.
    control.find('#' + ns + '_layerSelectorButton').click(function (e) {
	// Don't want the browser to scroll to # (top of page, and the 
	// href of the link) when the user unfolds the layer browser.
	e.preventDefault();

	if (jQuery('#' + ns + '_layerSelectorSlideBox').css('display') == 'none') {
	    // Not shown; show it
	    instance.log('showing layer browser');
	    jQuery('#' + ns + '_layerSelectorSlideBox').slideDown(500);
	}
	else {
	    // hide it
	    instance.log('hiding layer browser');
	    jQuery('#' + ns + '_layerSelectorSlideBox').slideUp(500);
	}

	// Set the width and height from the actual width and height
	// Otherwise it blocks dragging the map b/c it intercepts
	// drag events b/c the control has a higher z-index (see issue #2)

	// We get the current size of the span and set the div to be that
	// size. The span is not a block element, so it *should* scale with
	// the content.
	// This code doesn't seem to be necessary in all browsers
	// (but won't hurt); just having the <span> is enough for e.g.
	// Firefox and Chrome. I suspect it's needed in IE.
	// We set a timeout so that the function doesn't run until the 
	// box is fully retracted.
	setTimeout( function () {
	    var theControl = jQuery('#' + ns + '_layerSelector');
	    var theControlSpan = jQuery('#' + ns + '_layerSelector span');
	    theControl.height(theControlSpan.height() + 'px');
	    theControl.width(theControlSpan.width() + 'px');

	    // only redraw the search box if it's shown
	    if (instance.options.geocodeBox && instance.options.geocode) {
		// redraw the search box
		// WARNING WARNING WARNING: this is undocumented and may break
		// without notice!
		// we pop the search box using the index that we stored previously,
		// then re-add it.
		// removing it from the map
		var sBox = instance.map.controls[google.maps.ControlPosition.TOP_RIGHT].pop(this.searchBoxIndex);
		// update the index so we get the right one next time
		this.searchBoxIndex = instance.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(sBox);
	    }
	}, 550);
    });

    // now, the layer events
    // bind to the class so we just have one event handler that manages any
    // select/deselect
    control.find('.' + ns + '_layerSelectorBox').change(function (e) {
	// The checkbox that triggered this even
	var box = e.target;
	// don't use `this` - note above
	instance.log('toggling layer ' + box.value);
	if (box.checked) {
	    // toggle the layer
	    instance.showLayer(box.value);
	}
	else {
	    // toggle the layer
	    instance.hideLayer(box.value);
	}
    });

    // Add the control to the map
    this.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control[0]);

    // Add the search box, if requested
    if (this.options.geocodeBox && this.options.geocode) {
	var control = jQuery('\
<div id="' + ns + '_searchBox" class="fhWebMap_searchBox">\
<form action="#">\
<input type="text" width="20" value="Search for rooms..."></input>\
</form>\
</div>\
');
	// Bind the events
	// Reset when clicked
	control.find('input').bind('focus', function (e) {
	    if (this.value == 'Search for rooms...' | this.value == 'searching...' | this.value == 'not found') {
		this.value = '';
	    }
	});

	// Do the search
	control.find('form').submit(function (e) {
	    // Prevent the page from refreshing
	    e.preventDefault();
	    
	    var box = jQuery(this).find('input');
	    var room_no_raw = box.val();
	    box.val('searching...');

	    // Normalize it
	    var room_no = instance.normalizeInput(room_no_raw);

	    // We have to do some processing on the placemark and report to the user, so this is our
	    // callback
	    var draw = function (pmark) {
		var clearTextBox = function (e) {
		    // this gets called before the default action takes place,
		    // so whatever they were typing will get placed in the box
		    // in a few ms
		    this.value = '';
		}
		if (pmark == null) {
		    // User feedback via search box
		    box.val('not found');

		    // clear the box on the next focus or keypress
		    box.one('focus', clearTextBox);
		    box.one('click', clearTextBox);
		    box.one('keypress', clearTextBox);
		}
		else {
		    box.val('Search for rooms...');

		    box.one('focus', clearTextBox);
		    box.one('click', clearTextBox);
		    box.one('keypress', clearTextBox);

		    // Pass the scope through
		    instance.addPlacemarkToMap(pmark);
		}
	    }

	    // Find a placemark
	    instance.geocode(room_no, draw);
	});
	// Add the control
	// save the index so that we can redraw it
	this.searchBoxIndex = this.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control[0]);
    }

    
} // this.init

// Returns true if a layer is currently shown on a map
// the _ is taken from the Python convention of naming private functions
// with names starting with _; JavaScript has no way to create private 
// functions
fhWebMap.prototype._shown = function (layerName) {
    // deal with composite layer
    if (layerName == 'rooms') {
	// true if any or all rooms layers are shown
	return (this._shown('rooms1') | this._shown('rooms2') | this._shown('rooms3'))
    }
    else {
	if (this.layers[layerName].map == null) {
	    // not shown
	    return false;
	}
	else {
	    return true;
	}
    }
} // this.shown

//Check if an object is undefined in a browser-neutral way
// if you say x == undefined, some browsers will crash saying that you can't do a comparison to x b/c it is undefined
fhWebMap.prototype._undefined = function (obj) {
    if (typeof obj == 'undefined') {
        return true;
    }
    else {
        return false;
    }
}

fhWebMap.prototype.hideLayer = function (layerName) {
    this.log('hiding layer "' + layerName + '"');
    // rooms is a composite layer
    if (layerName == 'rooms') {
	this.hideLayer('rooms1');
	this.hideLayer('rooms2');
	this.hideLayer('rooms3');
    }
    else this.layers[layerName].setMap(null);
}

fhWebMap.prototype.showLayer = function (layerName) {
    this.log('showing layer "' + layerName + '"');
    var instance = this;
    /* Rooms are fun!
  Here's the idea here: if a user requests they want the rooms layer, they really want rooms3, rooms2 and rooms1, in that order (top-to-bottom).
  Unfortunately, layer order is undefined in Google Maps API. So, since setMap is asynchronous, we bind to the 'metadata_changed' event, which
  is only called once a layer has displayed. (thanks to http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/3911d373ef2bde5a among others).
  
  metadata_changed is only called correctly the first time we call setMap, so after that this won't work, but setMap isn't asynchronous anymore
  (or at least it's much faster, not sure which), so we can just call them in order and have them display in order.

  So, we show rooms1 and call ourselves back for rooms2 and rooms3
    */
    if (layerName == 'rooms') {
	// if this is the first load
	if (this.layers['rooms1'].getMetadata() == undefined) {
	    // daisy-chain the floors together, so that each one calls the next one once it has painted
	    google.maps.event.addListenerOnce(this.layers['rooms3'], 'metadata_changed', function () { instance.showLayer('rooms2'); });
	    google.maps.event.addListenerOnce(this.layers['rooms2'], 'metadata_changed', function () { instance.showLayer('rooms1'); });
	    // Set rooms1 as the layer in front
	    google.maps.event.addListenerOnce(this.layers['rooms1'], 'metadata_changed', function () { instance.layerInFront = 'rooms1'; });

	    // trigger elevators and restrooms after rooms1, if desired
	    // order of elevs/restrooms layers is undefined, but I don't
	    // think it matters.
	    if (jQuery.inArray('elevators', this.options.layers)) {
		google.maps.event.addListenerOnce(this.layers['rooms1'], 'metadata_changed', function () { instance.showLayer('elevators'); });
	    }

	    if (jQuery.inArray('restrooms', this.options.layers)) {
		google.maps.event.addListenerOnce(this.layers['rooms1'], 'metadata_changed', function () { instance.showLayer('restrooms'); });
	    }
	    
	    this.showLayer('rooms3');
	}
	else {
	    this.showLayer('rooms3');
	    this.showLayer('rooms2');
	    this.showLayer('rooms1');
      }
    }
    else {

	this.layers[layerName].setMap(this.map);
	
	// keep track of which layer is in the front
	// restrooms and elevators are *always* in front, so ignore them
	// parking doesn't overlap any other layers, so it doesn't need
	// layer control
	if (layerName != 'restrooms' & layerName != 'elevators' &
	    layerName != 'parking') {
	    this.layerInFront = layerName;
	}
    }

    // Make sure rooms are on top if we're messing with the building
    // footprint layer
    if (layerName == 'buildings' && this._shown('rooms')) {
	this.log('bringing "rooms" layer to foreground');
	this.bringToFront('rooms');
    }

    // Make sure that elevators and restrooms stay on top
    // it will get executed 3 times when you add the rooms layer, oh well
    if (layerName == 'rooms1' | layerName == 'rooms2' | layerName == 'rooms3' 
	| layerName == 'buildings') {
	if (this._shown('restrooms')) this.bringToFront('restrooms');
	if (this._shown('elevators')) this.bringToFront('elevators');	
    }
}

// Bring a layer to the top
// Always use this function instead of just calling showLayer or setMap,
// because it keeps track of state and won't redraw the map if it isn't
// necessary
fhWebMap.prototype.bringToFront = function (layerName) {  
    // if it isn't in front, bring it to the front
    if (this.layerInFront != layerName) { 
	this.showLayer(layerName);
    }
}

// Normalize text input, e.g. 'room 3401' -> '3401', '1001E8' -> '1001_E8'
// It also does searching, e.g. 'smithwick theater' -> '1001'
fhWebMap.prototype.normalizeInput = function (input) {
    // remove punctuation, so that if they entered, say, 1001_E8 it becomes
    // 1001E8 (underscore added back later)
    input = input.replace(/[_.-]/g, '');

    // Capitalize it (1001e8 to 1001E8)
    input = input.toUpperCase();

    // pull out the room no
    // the first number is 1-9, followed by exactly 3 digits 0-9,
    // optionally followed by a letter optionally followed by a number
    // get the first match
    var room_no = input.match(/[1-9][0-9]{3}[A-Z]{0,1}[0-9]{0,1}/);
    // No match: match gives us null; otherwise, set it to the first match.
    if (room_no != null) room_no = room_no[0];
    else {
	// do a search if we couldn't find a room number
	var listLen = this.searchDB.length;
	for (var i = 0; i < listLen; i++) {
	    // split out all the searches for a given room
	    var searchNames = this.searchDB[i][0].split(',');
	    var searchLen = searchNames.length;
	    // can't use i, nested loop
	    for (var j = 0; j < searchLen; j++) {
		// it's already capitalized to match the database,
		// use indexOf to find a substring (returns -1 if not present)
		if (input.indexOf(searchNames[j]) != -1) {
		    // return the room number
		    return this.searchDB[i][1];
		}
	    }
	}

	// Check they haven't searched for Middlefield Campus
	// it's after the other search in case, say, there was a place on
	// campus called McHenry Hall, and the search term was MCHENRY,
	// it would erroneously suggest that it was at Middlefield Campus
	// if we left it above the search feature
	if (input.indexOf('MC') != -1) {
	    this.alert('Room <b>' + input + '</b> is at ' +
		       '<a href="http://foothill.edu/middlefield/">' +
		       'Middlefield Campus</a>, located in Palo Alto about ' +
		       '6 miles north of Main Campus.');
	    return null;
	}

	// get out, can't postprocess further
	return room_no;
    }
    
    // Add an underscore if there is an extra letter/number
    // (e.g. 4103X, 1501_2)
    if (room_no.length > 4) {
	// insert it after the 4th letter
	room_no = room_no.slice(0, 4) + '_' + room_no.slice(4)
    }
    
    return room_no;
}

// Call callback with a placemark object for the given room number
// we also pass in the current value of this, because sometimes it gets lost
// when using other functions. If you don't need it in your callback, ignore it.
fhWebMap.prototype.geocode = function (room_no, callback) {
    var instance = this;

    // Check that the geocode DB has loaded
    // If not, wait and call again.
    if (this._undefined(this.geocodeDB)) {
	if (this.options.geocode == false) {
	    // database not loaded
	    this.alert('Cannot geocode; please set geocode to true in the map options hash!');
	}
	else {
	    // database currently loading
	    setTimeout(function () {
		// Call back same function, propagate params
		instance.geocode(room_no, callback);
	    }, 3000); // 3 secs
	}
	return null;
    }

    var pmark = this.geocodeDB.find('#' + room_no).first();
    if (pmark.length == 0) {
	// It returns null if we didn't find anything
	pmark = null;
    }
    else {
	pmark = pmark.get(0);
    }
    
    callback(pmark);
}

fhWebMap.prototype.addPlacemarkToMap = function (placemark) {
    var instance = this;
    // for convenience and efficiency, only jquerify once
    var pmark = jQuery(placemark);

    // make sure this room isn't already shown
    // we loop through this.placemarks because, if the legend has
    // wrapped past z back to replacing a, we don't want to claim its shown
    // if it has been removed
    // I know looping through the list and testing it probably isn't the most
    // efficient way to do this, but, with a max of 26 items, it's still pretty
    // fast
    // we loop over icons because they are a list; this.placemarks is an
    // associative array (an Object). When we get one that is undefined, we
    // stop because we've reached the end
    var iconsLen = this.icons.length;
    for (var i = 0; i < iconsLen; i++) {
	var roomOnMap = this.placemarks[i];
	// we've reached the end of the loop if we're getting undefined
	if (this._undefined(roomOnMap)) break;
	// roomOnMap has these attributes:
	// kml: a jQuerified KML <Placemark> element.
	// marker: the google.maps.Marker
	// if they have the same room_no
	if (roomOnMap.kml.find('SimpleData[name="room_no"]').text() == 
	    pmark.find('SimpleData[name="room_no"]').text()) {
	    // show the info window again by simulating a click event
	    google.maps.event.trigger(roomOnMap.marker, 'click');
	    // and end this function without adding a new Marker
	    return;
	}
    }

    // <coordinates> within <Point>
    var lonlat = pmark.find('Point coordinates').text().split(',');
    // KML is lon,lat while gmaps is lat,lon
    var pos = new google.maps.LatLng(Number(lonlat[1]), Number(lonlat[0]));

    // calculate the title
    // if there is no canonical name, just show the room no
    if (pmark.find('SimpleData[name="canname_en"]').text() == '') {
	var title = pmark.find('SimpleData[name="room_no"]').text();
    }
    else {
	var title = pmark.find('SimpleData[name="room_no"]').text() + ' (' +
	    pmark.find('SimpleData[name="canname_en"]').text() + ')';
    }

    var marker = new google.maps.Marker({
	position: pos,
	map: this.map,
	title: title,
	// Show the current icon. the iconIndex is updated below
	icon: this.icons[this.iconIndex],
	animation: google.maps.Animation.DROP
    });

    // Delete the previous placemark (if necessary)
    if (this.placemarks[this.iconIndex] != undefined) {
	this.placemarks[this.iconIndex].marker.setMap(null);
    }

    // and add the new, overwriting
    // we need the KML in there as well, for when we check if a particular room
    // number is already displayed
    // kml is a jQuerified KML Placemark element.
    this.placemarks[this.iconIndex] = {
	marker: marker,
	kml: pmark
    };

    // add it to the legend
    this.addPlacemarkToLegend(this.iconIndex, title);

    // use the next icon next time
    this.iconIndex++;
    if (this.iconIndex >= this.icons.length) this.iconIndex = 0;

    // Show an info window on click
    // use anonymous function to get pmark in the closure
    google.maps.event.addListener(marker, 'click', function () {
	instance.showInfoWindow(pmark, marker);
    });
   
    // And also show an info window *right now*
    this.showInfoWindow(pmark, marker);
 
    // We don't need to explicitly bring the layer to the front;
    // that will be done automatically by showInfoWindow
    // The reason it's in showInfoWindow and not here is because
    // the layer also needs to be brought to the front when we click on the
    // marker (either in the map or in the legend) or when we do another search
    // for the same room number
}

// show an info window with info about a room
// pmark is the jQuery placemark object, *not* the marker
// pmark may also a KmlFeatureData object, in which case marker is undefined
// and pmark should anchor the info window
fhWebMap.prototype.showInfoWindow = function (pmark, marker) {
    // Close the last info window
    if (this.infoWindow != undefined) this.infoWindow.close();

    // remove the null marker, if necessary
    if (this.nullMarker != undefined) {
	this.nullMarker.setMap(null);
	delete this.nullMarker;
    }

    // We're dealing a KML feature object from a map click
    if (pmark.featureData != undefined) {
	var source = 'kml';
	// as added by the XSLT
	// need to give a DOM element for reason described below
	var content = jQuery(pmark.featureData.description).get(0);

	// evidently, info windows must be anchored to a marker, so create a
	// marker using null.png, which is a 1x1px transparent png.
	// we call it this.nullMarker so it can be removed next time
	// showInfoWindow is called
	this.nullMarker = new google.maps.Marker({
	    position: pmark.latLng,
	    icon: this.urls.data + '/null.png',
	    map: this.map
	});
	var marker = this.nullMarker;
    }
    // else, we're dealing with a jQuerified KML placemark element from the
    // geocode DB
    else {
	var source = 'geocode';
	if (pmark.find('SimpleData[name="canname_en"]').text() == '') {
	    var title = pmark.find('SimpleData[name="room_no"]').text();
	}
	else {
	    var title = pmark.find('SimpleData[name="room_no"]').text() + ' (' +
		pmark.find('SimpleData[name="canname_en"]').text() + ')';
	}

	var bldglevel = pmark.find('SimpleData[name="bldglevel"]').text();

	// Bring the layer to the front, so that the info window appears 
	// over the correct room
	this.bringToFront('rooms' + bldglevel); // rooms1, rooms2 or rooms3

	if (bldglevel == '1') bldglevel = 'First floor';
	else if (bldglevel == '2') bldglevel = 'Second floor';
	else if (bldglevel == '3') bldglevel = 'Third floor';

	// build up the info window content
	// need to give DOM element, so that the text has rendered by the
	// time we put it into the info window. This way, the window will autosize
	// correctly.
	// see http://stackoverflow.com/questions/1554893/google-maps-api-v3-infowindow-not-sizing-correctly
	var content = jQuery('<div class="fhWebMap_infoWindow" id="' +
			     this.namespace + '_infoWindow">\
<b>' + title + '</b><br/>' + bldglevel + '<br/>\
<span class="fhWebMap_msg">' + pmark.find('SimpleData[name="msg_en"]').text() + '\
</span>\
</div>').get(0);
    }

    // show it on the map centered at the marker
    this.infoWindow = new google.maps.InfoWindow({
	content: content,
	maxWidth: 150 // another recommendation from the above URL to get the
	// text to flow correctly.
    });
    this.infoWindow.open(this.map, marker);		
}

// Open the legend if necessary, then add a placemark to it. index is the
// current value of iconIndex, 
fhWebMap.prototype.addPlacemarkToLegend = function (index, room_no) {
    if (this.options.legend) {
	var instance = this;
	var legend = jQuery('#' + this.namespace + '_legend');
	if (legend.css('display') == 'none') legend.show();

	// remove any other legend item using this icon
	legend.find('#' + this.namespace + '_legend_' + index).remove();

	// add it to the legend
	// there is a span with a space in it at the end to allow the legend
	// to wrap. Just a space is enough for Chrome, but Firefox needs to 
	// have a separate tag that isn't set to whitespace: nowrap
	// (as .fhWebMap_legendItem is)
	var legItem = jQuery('<span id="' + this.namespace + '_legend_' + 
			     index + '"' + 'class="fhWebMap_legendItem">' +
			     '<img height="22" src="' + this.icons[index] + 
			     '" /><b>' + room_no + '</b></span><span> </span>');

	// use mouseenter and mouseleave instead of mouseover and mouseout
	// because Chrome triggers a mouseout event immediately after the
	// mouseenter if there is more than one legend item.

	// make it bounce when user rolls over legend item
	legItem.mouseenter(function (e) {
	    // index is in the closure
	    instance.placemarks[index].marker.setAnimation(google.maps.Animation.BOUNCE);
	});

	// and make it stop when they roll out
	legItem.mouseleave(function (e) {
	    // index is in the closure
	    instance.placemarks[index].marker.setAnimation(null);
	});

	// if they click a legend item, pass the click on to that marker
	// this will make an info window open.
	// the reasons for doing this instead of calling showInfoWindow 
	// from here are twofold:
	// 0) It is very DRY in programming principle; i.e. there is no repeated
	//    code
	// 1) The placemark and the marker are not available in the context of
	//    this function.
	legItem.click(function (e) {
	    // index is in the closure
	    google.maps.event.trigger(instance.placemarks[index].marker, 'click');
	});

	legend.append(legItem);

	// resize the map to fit the legend
	// This is the height of our entire box, everything we have to play
	// with
	var ourHeight = jQuery('#' + this.mapElemId).height();
	var legHeight = legend.height();
	jQuery('#' + this.namespace + '_map').height(ourHeight - legHeight);
    }
}

/*I know, I know, I shouldn't mix data and code. But this really is the
 easiest way to do it, and will load quickly while avoiding same-origin
 issues
 Here's how I made this:
   1. I ran this SQL (PostGIS, column names will be capitalized in Arc)
      SELECT search_en, room_no FROM rooms WHERE search_en != '';
   2. I copied the output into a text file that looked like this
 HEALTH SERVICES  | 2126
 POLICE           | 2106
 BOOKSTORE        | 2301
 COUNSEL          | 8305_A
 CAMPUS CENTER    | 2201
 KRAUSE,KCI       | 4001
 APPRECIATION     | 1501
 ADMISSION,RECORD | 8101
 LOHMAN           | 8001
 LIBRARY          | 3501
 SMITHWICK        | 1001
 CAFETERIA        | 2211
   3. I ran it through this sed script:
      sed 's/^ /\["/g' | sed 's/ .*| /","/' | sed 's/$/"\],/'
   4. I wrapped [ ] around that and pasted it here, removing the final comma
      after [...,"2211"],

*/

fhWebMap.prototype.searchDB = [
    ["DRC,DISABILITY","5801"],
    ["ISC","3617"],
    ["TOYON","2020"],
    ["ALTOS","2019"],
    ["HEALTH","2126"],
    ["POLICE","2106"],
    ["BOOKSTORE","2301"],
    ["COUNSEL","8305_A"],
    ["CAMPUS","2201"],
    ["KRAUSE,KCI","4001"],
    ["APPRECIATION","1501"],
    ["ADMISSION,RECORD","8101"],
    ["LOHMAN","8001"],
    ["LIBRARY","3501"],
    ["SMITHWICK","1001"],
    ["CAFETERIA","2211"]
];

// URLs, which are specific to each deployment
fhWebMap.prototype.urls = {
    data: 'http://foothill-web-gis.googlecode.com/svn/tags/data-0.8'
};

// This is a list of the icons to use. It will be looped through in order.
fhWebMap.prototype.icons = [
    // we aren't in an instance, so use fhWebMap.prototype
    fhWebMap.prototype.urls.data + '/icons/a.png',
    fhWebMap.prototype.urls.data + '/icons/b.png',
    fhWebMap.prototype.urls.data + '/icons/c.png',
    fhWebMap.prototype.urls.data + '/icons/d.png',
    fhWebMap.prototype.urls.data + '/icons/e.png',
    fhWebMap.prototype.urls.data + '/icons/f.png',
    fhWebMap.prototype.urls.data + '/icons/g.png',
    fhWebMap.prototype.urls.data + '/icons/h.png',
    fhWebMap.prototype.urls.data + '/icons/i.png',
    fhWebMap.prototype.urls.data + '/icons/j.png',
    fhWebMap.prototype.urls.data + '/icons/k.png',
    fhWebMap.prototype.urls.data + '/icons/l.png',
    fhWebMap.prototype.urls.data + '/icons/m.png',
    fhWebMap.prototype.urls.data + '/icons/n.png',
    fhWebMap.prototype.urls.data + '/icons/o.png',
    fhWebMap.prototype.urls.data + '/icons/p.png',
    fhWebMap.prototype.urls.data + '/icons/q.png',
    fhWebMap.prototype.urls.data + '/icons/r.png',
    fhWebMap.prototype.urls.data + '/icons/s.png',
    fhWebMap.prototype.urls.data + '/icons/t.png',
    fhWebMap.prototype.urls.data + '/icons/u.png',
    fhWebMap.prototype.urls.data + '/icons/v.png',
    fhWebMap.prototype.urls.data + '/icons/w.png',
    fhWebMap.prototype.urls.data + '/icons/x.png',
    fhWebMap.prototype.urls.data + '/icons/y.png',
    fhWebMap.prototype.urls.data + '/icons/z.png'
];